﻿// -----------------------------------------------------------------------
// <copyright file="GameModel.cs" company="R&R">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

#region Imports
using System;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Linq;
using System.Windows;
using System.Collections.Generic;
#endregion Imports

namespace Domino
{
    /// <summary>
    /// TODO: Update summary.
    /// </summary>
    public class GameModel : BaseViewModel
    {
        #region Command declarations
        public RelayCommand PassCommand { get; set; }
        #endregion Command declarations

        #region Properties
        private eGameType gameType;
        public eGameType GameType
        {
            get { return gameType; }
            set { gameType = value; RaisePropertyChanged("GameType"); }
        }

        private ObservableCollection<TileModel> boneYard = new ObservableCollection<TileModel>();
        public ObservableCollection<TileModel> BoneYard
        {
            get { return boneYard; }
            set { boneYard = value; RaisePropertyChanged("BoneYard"); }
        }

        private PlayerModel currentPlayer;
        public PlayerModel CurrentPlayer
        {
            get { return currentPlayer; }
            set
            {
                currentPlayer = value; RaisePropertyChanged("CurrentPlayer");
                if (currentPlayer != null)
                {
                    PlayerTiles.ToList().ForEach(p =>
                    {
                        p.IsCurrent = (p == currentPlayer);
                        p.Tiles_org.ToList().ForEach(t => { if (!p.IsCurrent) t.IsPlayable = false; });
                    }); // update other players

                    // update current player
                    if (Board.Any())
                    {
                        var left = GetBoardEnds(eConnectingTileEnd.Left);
                        var right = GetBoardEnds(eConnectingTileEnd.Right);
                        currentPlayer.Tiles_org.ToList().ForEach(t => ValidatePlayable(t, left, right));
                    }
                    else // first play
                    {
                        var pairs = currentPlayer.Tiles_org.Where(t => t.IsPair);
                        if (pairs.Any())
                        {
                            var maxPairValue = pairs.Select(t => t.LeftTileValue).Max();
                            currentPlayer.Tiles.ToList().ForEach(t => t.IsPlayable = (t.IsPair && t.LeftTileValue == maxPairValue));
                        }
                        else currentPlayer.Tiles.ToList().ForEach(t => t.IsPlayable = true);
                    }

                    ValidateBoneyard();
                }
            }
        }

        private PlayerModel playerRotation;
        public PlayerModel PlayerRotation
        {
            get { return playerRotation; }
            set { playerRotation = value; RaisePropertyChanged("PlayerRotation"); }
        }

        private bool isBoneyardEnabled;
        public bool IsBoneyardEnabled
        {
            get { return isBoneyardEnabled; }
            set { isBoneyardEnabled = value; RaisePropertyChanged("IsBoneyardEnabled"); }
        }

        private ObservableCollection<PlayerModel> playerTiles = new ObservableCollection<PlayerModel>();
        public ObservableCollection<PlayerModel> PlayerTiles
        {
            get { return playerTiles; }
            set { playerTiles = value; RaisePropertyChanged("PlayerTiles"); }
        }

        private ObservableCollection<TileModel> board = new ObservableCollection<TileModel>();
        public ObservableCollection<TileModel> Board
        {
            get { return board; }
            set
            {
                board = value; RaisePropertyChanged("Board");

                // HARDCODE
                var compressBoardText = String.Empty;
                var boardText = String.Empty;
                if (board != null)
                {
                    var z1 = new CompressBoardConverter().Convert(board, typeof(ObservableCollection<TileModel>), 3, null);
                    ((ObservableCollection<TileModel>)z1).ToList().ForEach(t => compressBoardText += (t.IsNullValue ? "( ... )" : t.ToString()) + " - ");

                    var z2 = new CompressBoardConverter().Convert(board, typeof(ObservableCollection<TileModel>), 0, null);
                    ((ObservableCollection<TileModel>)z2).ToList().ForEach(t => boardText += (t.IsNullValue ? "( ... )" : t.ToString()) + " - ");
                }
                CompressBoardText = compressBoardText.TrimEnd("- ".ToCharArray());
                BoardText = boardText.TrimEnd("- ".ToCharArray());
            }
        }

        private int boardValue;
        public int BoardValue
        {
            get { return boardValue; }
            set { boardValue = value; RaisePropertyChanged("BoardValue"); }
        }

        private eTileOrientation boneyardOrientation = eTileOrientation.Vertical;
        public eTileOrientation BoneyardOrientation
        {
            get { return boneyardOrientation; }
            set { boneyardOrientation = value; RaisePropertyChanged("BoneyardOrientation"); }
        }

        private eTileOrientation playerOrientation = eTileOrientation.Vertical;
        public eTileOrientation PlayerOrientation
        {
            get { return playerOrientation; }
            set { playerOrientation = value; RaisePropertyChanged("PlayerOrientation"); }
        }

        private eTileOrientation boardOrientation = eTileOrientation.Horizontal;
        public eTileOrientation BoardOrientation
        {
            get { return boardOrientation; }
            set { boardOrientation = value; RaisePropertyChanged("BoardOrientation"); }
        }

        private eTileOrientation boardOrientationPair = eTileOrientation.Vertical;
        public eTileOrientation BoardOrientationPair
        {
            get { return boardOrientationPair; }
            set { boardOrientationPair = value; RaisePropertyChanged("BoardOrientationPair"); }
        }

        private bool allowNextRound;
        public bool AllowNextRound
        {
            get { return allowNextRound; }
            set { allowNextRound = value; RaisePropertyChanged("AllowNextRound"); }
        }

        private bool allowHint;
        public bool AllowHint
        {
            get { return allowHint; }
            set { allowHint = value; RaisePropertyChanged("AllowHint"); }
        }
        #endregion Properties

        private string compressBoardText;
        /// <summary>
        /// HARDCODE temp code
        /// </summary>
        public string CompressBoardText
        {
            get { return compressBoardText; }
            set { compressBoardText = value; RaisePropertyChanged("CompressBoardText"); }
        }

        private string boardText;
        /// <summary>
        /// HARDCODE temp code
        /// </summary>
        public string BoardText
        {
            get { return boardText; }
            set { boardText = value; RaisePropertyChanged("BoardText"); }
        }

        #region Constructors
        public GameModel(ObservableCollection<PlayerModel> playerNames, PlayerModel lastPlayer, eGameType gameTpye)
        {
            this.PlayerTiles = playerNames;
            this.PlayerRotation = playerRotation;
            this.GameType = gameTpye;

            if (this.GameType != eGameType.Fives)
                throw new NotImplementedException("Game type not implemented yet!");

            PlayerRotation = lastPlayer; // Set last player; Starting Player to be set in StartGame
            PassCommand = new RelayCommand(new Action<object>((parameter) => { SetNextPlayer(); }));
        }
        #endregion Constructors

        #region Methods
        internal void StartGame(bool isNewGame)
        {
            AllowNextRound = false;

            IntializeBoard(isNewGame); //initialize the board
            DistributeTiles(); // distribute tiles to players

            PlayerRotation = NextPlayer(PlayerRotation);
            SetNextPlayer(); // set first player
        }

        private void UpdatePlayerTiles(TileModel tileModel)
        {
            tileModel.TileLocation = eTileLocation.Board;
            PlayerTiles.ToList().ForEach(pl => pl.RefreshPlayerTiles());
        }

        public void AddToPlayerFromBoneyard(TileModel tileModel)
        {
            BoneYard.Remove(tileModel);

            tileModel.TileLocation = eTileLocation.Player;
            tileModel.TileOrientation = PlayerOrientation;
            ValidatePlayable(tileModel);

            CurrentPlayer.Tiles_org.Add(tileModel);
            CurrentPlayer.RefreshPlayerTiles();

            ValidateBoneyard();
        }

        internal void AddToBoardFromPlayer(TileModel tileModel)
        {
            if (Board.Any())
            {
                var tileEndIndecisive = false;
                var left = GetBoardEnds(eConnectingTileEnd.Left);
                var right = GetBoardEnds(eConnectingTileEnd.Right);

                // check if can be added to either side
                if ((tileModel.RightTileValue == left || tileModel.LeftTileValue == left) && (tileModel.RightTileValue == right || tileModel.LeftTileValue == right))
                {
                    tileEndIndecisive = true;
                    if (Board.Count() == 1 || (left == right && tileModel.IsPair)
                        || (left == right & !Board.First().IsPair && !Board.Last().IsPair)) tileEndIndecisive = false;
                }

                eConnectingTileEnd connectingTileEnd = eConnectingTileEnd.None;
                if (tileEndIndecisive)
                {
                    string msg = "Do you want the domino " + tileModel + " at the start?\r\n\r\nClick YES to add at the start\r\nClick NO to add in the end";
                    if (MessageBox.Show(msg, "An intelligent check..", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
                        connectingTileEnd = eConnectingTileEnd.Left;
                    else connectingTileEnd = eConnectingTileEnd.Right;
                }
                else // connecting end - not indecisive
                {
                    if (tileModel.RightTileValue == left || tileModel.LeftTileValue == left) // check if can be added to left
                        connectingTileEnd = eConnectingTileEnd.Left;
                    else if (tileModel.RightTileValue == right || tileModel.LeftTileValue == right) // check if can be added to right
                        connectingTileEnd = eConnectingTileEnd.Right;
                }

                if (connectingTileEnd == eConnectingTileEnd.Left) // check if can be added to left
                {
                    if (tileModel.LeftTileValue == left) SwapTileValues(tileModel);
                    AddTileToBoard(tileModel, true);
                }
                else if (connectingTileEnd == eConnectingTileEnd.Right) // check if can be added to right
                {
                    if (tileModel.RightTileValue == right) SwapTileValues(tileModel);
                    AddTileToBoard(tileModel);
                }
                else MessageBox.Show("Don't be an ASSHOLE and try to play a VALID tile!", "God's perfect validation..", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            else AddTileToBoard(tileModel); // add first tile
        }

        private void AddTileToBoard(TileModel tileModel, bool atStart = false)
        {
            UpdatePlayerTiles(tileModel);
            tileModel.TileOrientation = tileModel.IsPair ? BoardOrientationPair : BoardOrientation;
            tileModel.IsPlayable = true;

            if (atStart) board.Insert(0, tileModel); // add at beginning
            else board.Add(tileModel); // add at end

            UpdateScore(); // update player score

            if (CurrentPlayer.Tiles.None()) // current player says Domino
            {
                MessageBox.Show(String.Format("Domino! '{0}' wins this round.", CurrentPlayer.Name));
                EndRound();

                return;
            }
            else
            {
                SetNextPlayer(); // set next player only if current player not Domino

                // no tiles to play and boneyard is empty
                ValidateIfTilesBlocked();
            }

            Board = board;
        }

        private void ValidateIfTilesBlocked()
        {
            if (!IsBoneyardEnabled && !CurrentPlayer.IsPassEnabled && CurrentPlayer.Tiles.Where(t => t.IsPlayable).None())
            {
                MessageBox.Show(String.Format("All tiles blocked. Game ends here!"));
                EndRound();
            }
        }

        private void EndRound()
        {
            UpdateResidualScore();
            AllowNextRound = true;

            switch (GameType)
            {
                case eGameType.Fives:
                    PlayerTiles.ToList().ForEach(p => p.Score += p.BoardScore); // update board score as earned in game
                    if (CurrentPlayer.Tiles.None()) // tiles exhausted
                    {
                        int totalOthersResidual = 0;
                        PlayerTiles.Where(p => p != CurrentPlayer).ToList().ForEach(p => totalOthersResidual += p.ResidualScore);
                        CurrentPlayer.Score += totalOthersResidual;
                    }
                    else // tiles blocked
                    {
                        var minResidual = PlayerTiles.Select(p => p.ResidualScore).Min(); // minimum residual value
                        var winners = PlayerTiles.Where(p => p.ResidualScore == minResidual); // players minimum residual value

                        int totalOthersResidual = 0;
                        PlayerTiles.Where(p => !winners.Contains(p)).ToList().ForEach(p => totalOthersResidual += p.ResidualScore); // residual value of other players

                        totalOthersResidual = totalOthersResidual / winners.Count(); // divide residual values in case of draw
                        winners.ToList().ForEach(p => p.Score = p.BoardScore + totalOthersResidual - minResidual);
                    }
                    break;

                case eGameType.Block:
                case eGameType.Draw:
                    throw new NotImplementedException("Game type not implemented yet!");

                default:
                    throw new NotImplementedException("Game type not defined!");
            }
        }

        private void UpdateScore()
        {
            switch (GameType)
            {
                case eGameType.Fives:
                    if (Board.Count() == 1 && Board.First().IsPair) BoardValue = GetBoardEndValue(eConnectingTileEnd.Left);
                    else BoardValue = GetBoardEndValue(eConnectingTileEnd.Left) + GetBoardEndValue(eConnectingTileEnd.Right);

                    if (BoardValue % 5 == 0) CurrentPlayer.BoardScore += BoardValue;
                    break;

                case eGameType.Block:
                case eGameType.Draw:
                    throw new NotImplementedException("Game type not implemented yet!");

                default:
                    throw new NotImplementedException("Game type not defined!");
            }
        }

        private void UpdateResidualScore()
        {
            switch (GameType)
            {
                case eGameType.Fives:
                    PlayerTiles.ToList().ForEach(p => p.ResidualScore = AddTileValues(p.Tiles));
                    break;

                case eGameType.Block:
                case eGameType.Draw:
                    throw new NotImplementedException("Game type not implemented yet!");

                default:
                    throw new NotImplementedException("Game type not defined!");
            }
        }

        private int AddTileValues(ObservableCollection<TileModel> tiles)
        {
            var tileValue = 0;
            switch (GameType)
            {
                case eGameType.Fives:
                    // TODO: Add to winner
                    tiles.ToList().ForEach(t => tileValue += Convert.ToInt32(Math.Round(t.TileTotalValue / 5.0, 0)) * 5);
                    break;

                case eGameType.Block:
                case eGameType.Draw:
                    throw new NotImplementedException("Game type not implemented yet!");

                default:
                    throw new NotImplementedException("Game type not defined!");
            }

            return tileValue;
        }

        /// <summary>
        /// Gets the value of tile at the end of the board
        /// </summary>
        /// <param name="connectingTileEnd">Tile end to validate</param>
        /// <returns>Tile value of the specified end</returns>
        /// <exception cref="System.ArgumentException">When board does not contain any tiles</exception>
        private int GetBoardEnds(eConnectingTileEnd connectingTileEnd)
        {
            switch (connectingTileEnd)
            {
                case eConnectingTileEnd.Left:
                    return Board.First().LeftTileValue;

                case eConnectingTileEnd.Right:
                    return Board.Last().RightTileValue;

                case eConnectingTileEnd.None:
                default:
                    throw new InvalidOperationException();
            }
        }

        private int GetBoardEndValue(eConnectingTileEnd connectingTileEnd)
        {
            switch (connectingTileEnd)
            {
                case eConnectingTileEnd.Left:
                    return Board.First().LeftTileValue * (Board.First().IsPair ? 2 : 1);

                case eConnectingTileEnd.Right:
                    return Board.Last().RightTileValue * (Board.Last().IsPair ? 2 : 1);

                case eConnectingTileEnd.None:
                default:
                    throw new InvalidOperationException();
            }
        }

        /// <summary>
        /// Sets next player
        /// </summary>
        internal void SetNextPlayer()
        {
            if (CurrentPlayer == null) CurrentPlayer = PlayerRotation; // first tile
            else CurrentPlayer = NextPlayer(CurrentPlayer); // current game
        }

        private PlayerModel NextPlayer(PlayerModel currentPlayer)
        {
            if (currentPlayer == null || currentPlayer == PlayerTiles.Last()) currentPlayer = PlayerTiles.First();
            else currentPlayer = PlayerTiles.ElementAt(PlayerTiles.IndexOf(currentPlayer) + 1);

            return currentPlayer;
        }

        /// <summary>
        /// Swaps tile values
        /// </summary>
        /// <param name="tileModel">Tile</param>
        private void SwapTileValues(TileModel tileModel)
        {
            var left = tileModel.LeftTileValue;
            tileModel.LeftTileValue = tileModel.RightTileValue;
            tileModel.RightTileValue = left;
        }

        /// <summary>
        /// Distributes tiles to players
        /// </summary>
        private void DistributeTiles()
        {
            int numberOfPlayers = PlayerTiles.Count();

            var totalChipCount = BoneYard.Count;
            var rand = new Random();
            var currChip = rand.Next(0, totalChipCount);
            int maxChipCount = GetMaxChipCount(numberOfPlayers);

            playerTiles.ToList().ForEach(p => p.Tiles_org = new ObservableCollection<TileModel>());
            for (int i = 1; i <= maxChipCount; i++)
            {
                foreach (var player in playerTiles)
                {
                    //assign tiles to players
                    var tile = BoneYard.ElementAt(currChip);
                    tile.TileLocation = eTileLocation.Player;
                    tile.TileOrientation = PlayerOrientation;

                    player.Tiles_org.Add(tile);
                    BoneYard.Remove(tile);

                    totalChipCount--;
                    currChip = rand.Next(0, totalChipCount);
                }
            }

            PlayerTiles = playerTiles;
        }

        /// <summary>
        /// Initializes board
        /// </summary>
        /// <param name="isNewGame">TRUE: New game, else new round</param>
        internal void IntializeBoard(bool isNewGame)
        {
            if (isNewGame) PlayerTiles.ToList().ForEach(p => p.Score = 0);
            PlayerTiles.ToList().ForEach(p => { p.BoardScore = 0; p.ResidualScore = 0; });

            int defMaxDotValue = 6;
            int maxDotValue = -1;
            if (!Int32.TryParse(ConfigurationManager.AppSettings["Dots"], out maxDotValue)) maxDotValue = defMaxDotValue;
            if (!"6,9".Split(',').Contains(maxDotValue.ToString())) maxDotValue = defMaxDotValue;

            var boneYard = new ObservableCollection<TileModel>();
            for (int i = 0; i <= maxDotValue; i++)
            {
                for (int j = 0; j <= maxDotValue; j++)
                {
                    //add tiles to the board
                    //check if tile is already added
                    var nextTile = new TileModel(i, j) { TileLocation = eTileLocation.BoneYard, TileOrientation = BoneyardOrientation, AllowHint = this.AllowHint };
                    if (!boneYard.Contains(nextTile)) boneYard.Add(nextTile);
                }
            }

            RandomizeTiles(boneYard);
            BoneYard = boneYard;
        }

        internal TileModel PickRandomTile(IEnumerable<TileModel> tiles)
        {
            var rand = new Random();
            return tiles.ElementAtOrDefault(rand.Next(0, tiles.Count()));
        }

        internal void RandomizeTiles(ObservableCollection<TileModel> tiles)
        {
            var yard = new ObservableCollection<TileModel>();
            while (tiles.Any())
            {
                var rand = new Random();
                var tile = tiles.ElementAt(rand.Next(0, tiles.Count()));

                tiles.Remove(tile); yard.Add(tile);
            }

            yard.ToList().ForEach(t => tiles.Add(t));
        }

        private int GetMaxChipCount(int numberOfPlayers)
        {
            switch (numberOfPlayers)
            {
                case 2: return 7;
                case 3: return 6;
                case 4: return 5;
                default: throw new InvalidOperationException("2-4 players can play the game.");
            }
        }

        private void ValidatePlayable(TileModel tileModel, int left = -1, int right = -1)
        {
            if (left < 0) left = GetBoardEnds(eConnectingTileEnd.Left);
            if (right < 0) right = GetBoardEnds(eConnectingTileEnd.Right);

            tileModel.IsPlayable = (tileModel.TileLocation == eTileLocation.Board) || (tileModel.LeftTileValue == left || tileModel.RightTileValue == left
                || tileModel.LeftTileValue == right || tileModel.RightTileValue == right);
        }

        private void ValidateBoneyard()
        {
            IsBoneyardEnabled = CurrentPlayer.Tiles.Where(t => t.IsPlayable).None();
            BoneYard.ToList().ForEach(t => t.IsPlayable = IsBoneyardEnabled);

            CurrentPlayer.IsPassEnabled = IsBoneyardEnabled && BoneYard.None(); // Enable Pass if boneyad is exhausted and no tiles to play
            ValidateIfTilesBlocked();
        }
        #endregion Methods
    }
}
